package data;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.HashMap;

import main.Core;

import session.Session;

import configuration.Paths;

/**
 * A generic singleton database for holding data objects and providing access to
 * them through string references.
 * 
 * @author Richard
 */
public class ObjectDatabase {

	/*
	 * The singleton instance of the object database.
	 */
	private static ObjectDatabase instance = null;

	/*
	 * Holds data objects based on class name and then object guid.
	 */
	private HashMap<String, HashMap<String, DataObject>> dataObjects;

	/*
	 * Holds editor objects based on class name and then object guid.
	 */
	private HashMap<String, HashMap<String, EditorObject>> editorObjects;

	/*
	 * Singleton constructor that instantiates the data objects collection.
	 */
	private ObjectDatabase() {
		dataObjects = new HashMap<String, HashMap<String, DataObject>>();
		editorObjects = new HashMap<String, HashMap<String, EditorObject>>();
	}

	/**
	 * Singleton accessor for the static instance of ObjectDatabase.
	 * 
	 * @return The static instance.
	 */
	public static ObjectDatabase getInstance() {
		if (instance == null) {
			instance = new ObjectDatabase();
		}
		return instance;
	}

	/**
	 * Saves all data objects owned by the current project in the project's data
	 * file.
	 */
	public void save() {
		try {
			// Open the project data file
			File projectDataFile = new File(Paths.projectDataPath
					+ File.separator
					+ Session.getInstance().getProjectName()
					+ ".rpg");
			if (!projectDataFile.exists()) {
				projectDataFile.createNewFile();
			}

			// Filter for only data objects owned by the current project
			ArrayList<DataObject> projectDataObjects = new ArrayList<DataObject>();
			for (String className : dataObjects.keySet()) {
				for (String objectName : dataObjects.get(className).keySet()) {
					// If the owner matches the current project, add to list
					if (dataObjects.get(className).get(objectName).getOwner()
							.equals(Session.getInstance().getProjectName())) {
						projectDataObjects.add(dataObjects.get(className).get(
								objectName));
					}
				}
			}

			// Write the filtered list to file
			FileOutputStream fos = new FileOutputStream(projectDataFile);
			ObjectOutputStream oos = new ObjectOutputStream(fos);
			oos.writeObject(projectDataObjects);
			oos.close();
			fos.close();
		} catch (FileNotFoundException e) {
			Core.getInstance().logMessage(
					ObjectDatabase.class.getName(),
					"Could not find project data file for project "
							+ Session.getInstance().getProjectName(), e);
		} catch (IOException e) {
			Core.getInstance().logMessage(
					ObjectDatabase.class.getName(),
					"Error saving data for project "
							+ Session.getInstance().getProjectName(), e);
		}
	}

	/**
	 * Loads all data objects from the current project's data file in addition
	 * too all shared data objects from other data files. Warning: This method
	 * clears all data currently loaded without saving changes.
	 */
	public void load() {
		// Load all project and shared data objects
		ArrayList<DataObject> temp = new ArrayList<DataObject>();
		File projectDataDir = new File(Paths.projectDataPath);
		for (File f : projectDataDir.listFiles()) {
			if (!f.isDirectory() && f.getName().contains(".rpg")) {
				// Default to unowned for case when project name is null
				// (no project is currently loaded)
				boolean fileOwnedByCurrentProject = false;
				if (Session.getInstance().getProjectName() != null) {
					fileOwnedByCurrentProject = f.getName().substring(0,
							f.getName().lastIndexOf('.')).equals(
							Session.getInstance().getProjectName());
				}
				// Only get shared data if file is not owned by current project
				temp.addAll(this.getProjectDataObjects(f,
						!fileOwnedByCurrentProject));
			}
		}

		// Build the hash map for local representation
		dataObjects.clear();
		editorObjects.clear();
		for (DataObject o : temp) {
			// Create matching editor object and data object
			this.addEditorObject(o);
		}
	}

	/*
	 * Retrieves all data objects from the given project data file. If the
	 * shared parameter is true, only shared objects are returned from the given
	 * file.
	 */
	@SuppressWarnings("unchecked")
	private ArrayList<DataObject> getProjectDataObjects(File projectDataFile,
			boolean onlyLoadShared) {
		try {
			FileInputStream fis = new FileInputStream(projectDataFile);
			ObjectInputStream ois = new ObjectInputStream(fis);
			ArrayList<DataObject> data = (ArrayList<DataObject>) ois
					.readObject();
			// If we have been asked to only load shared objects, do so
			if (onlyLoadShared) {
				ArrayList<DataObject> filteredData = new ArrayList<DataObject>();
				for (DataObject o : data) {
					if (o.isShared()) {
						filteredData.add(o);
					}
				}
				data = filteredData;
			}
			ois.close();
			fis.close();
			return data;
		} catch (FileNotFoundException e) {
			Core.getInstance().logMessage(
					ObjectDatabase.class.getName(),
					"Could not find project data file for project "
							+ Session.getInstance().getProjectName(), e);
		} catch (IOException e) {
			Core.getInstance().logMessage(
					ObjectDatabase.class.getName(),
					"Error loading data for project "
							+ Session.getInstance().getProjectName(), e);
		} catch (ClassNotFoundException e) {
			Core.getInstance().logMessage(
					ObjectDatabase.class.getName(),
					"Casting error to ArrayList<DataObject> when loading project "
							+ Session.getInstance().getProjectName(), e);
		}
		return null;
	}

	/**
	 * Adds the given object the data collection using the object's class and
	 * then it's name. If the class doesn't exist, it is added. If the name of
	 * this object already exists in the same class category, null is returned.
	 * Only called when creating a new editor object. Uses the given data object
	 * if it exists instead of creating a new one.
	 * 
	 * @param className
	 * @param guid
	 * @param o
	 * @return DataObject The object added. Returns null if the object already
	 *         exists.
	 */
	private DataObject addDataObject(String fqClassName, String guid,
			DataObject o) {
		if (dataObjects.containsKey(fqClassName)) {
			if (dataObjects.get(fqClassName).containsKey(guid)) {
				return null;
			}
		} else {
			dataObjects.put(fqClassName, new HashMap<String, DataObject>());
		}
		DataObject object;
		if (o == null) {
			object = new DataObject(fqClassName, guid, Session.getInstance()
					.getProjectName());
		} else {
			object = o;
		}
		dataObjects.get(fqClassName).put(guid, object);
		return object;
	}

	/**
	 * Overloaded version of addEditorObject for loading in existing Data
	 * Objects.
	 * 
	 * @param className
	 * @param name
	 * @param loadedObject
	 * @return EditorObject The object added. Returns null if the object already
	 *         exists.
	 */
	private EditorObject addEditorObject(DataObject loadedObject) {
		if (editorObjects.containsKey(loadedObject.getFQClassName())) {
			if (editorObjects.get(loadedObject.getFQClassName()).containsKey(
					loadedObject.getGUID())) {
				return null;
			}
		} else {
			editorObjects.put(loadedObject.getFQClassName(),
					new HashMap<String, EditorObject>());
		}
		EditorObject object = this.createEditorObjectFromClass(loadedObject
				.getFQClassName(), loadedObject.getGUID(), loadedObject);
		if (object != null) {
			editorObjects.get(loadedObject.getFQClassName()).put(
					loadedObject.getGUID(), object);
		}
		return object;
	}

	/**
	 * Adds the given object the data collection.If the class doesn't exist, it
	 * is added. If the name of this object already exists in the same class
	 * category, null is returned.
	 * 
	 * @param className
	 * @param name
	 * @return EditorObject The object added. Returns null if the object already
	 *         exists.
	 */
	public EditorObject addEditorObject(String fqClassName) {
		String guid = ObjectDatabase.generateGUID("EditorObject");
		if (editorObjects.containsKey(fqClassName)) {
			if (editorObjects.get(fqClassName).containsKey(guid)) {
				return null;
			}
		} else {
			editorObjects.put(fqClassName, new HashMap<String, EditorObject>());
		}
		EditorObject object = this.createEditorObjectFromClass(fqClassName,
				guid, null);
		if (object != null) {
			editorObjects.get(fqClassName).put(guid, object);
		}
		return object;
	}

	/**
	 * Accessor to retrieve certain data objects of name and class. These
	 * objects are private to the matching editor object. Access data only
	 * through accessors of other editor objects.
	 * 
	 * @return DataObject The object requested. Returns null if no matching
	 *         object exists.
	 */
	public DataObject getDataObject(String fqClassName, String objectName) {
		if (dataObjects.containsKey(fqClassName)) {
			return dataObjects.get(fqClassName).get(objectName);
		}
		return null;
	}

	/**
	 * Returns map of all editor objects of the given class name.
	 * 
	 * @param fqClassName
	 *            The class name.
	 * @return HashMap The map of matching editor objects.
	 */
	public HashMap<String, EditorObject> getAllEditorObjectsOfClass(
			String fqClassName) {
		if (editorObjects.containsKey(fqClassName)) {
			return editorObjects.get(fqClassName);
		}
		return null;
	}

	/**
	 * Returns true if a data object of the given display name and class name
	 * exists.
	 * 
	 * @param fqClassName
	 *            The class name.
	 * @return boolean
	 */
	public boolean dataObjectExistsOfName(String fqClassName, String displayName) {
		for (EditorObject editorObject : this.getAllEditorObjectsOfClass(
				fqClassName).values()) {
			if (editorObject.getData().getDisplayName().equals(displayName)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Accessor to retrieve certain editor objects of guid and class.
	 * 
	 * @return EditorObject The object requested. Returns null if no matching
	 *         object exists.
	 */
	public EditorObject getEditorObject(String fqClassName, String guid) {
		if (editorObjects.containsKey(fqClassName)) {
			return editorObjects.get(fqClassName).get(guid);
		}
		return null;
	}

	/*
	 * Creates a new EditorObject instance with the given class name and guid.
	 * The DataObject argument is used when an editor object is being loaded.
	 */
	private EditorObject createEditorObjectFromClass(String fqClassName,
			String guid, DataObject o) {
		try {
			EditorObject editorObject = (EditorObject) Class.forName(
					fqClassName).newInstance();
			editorObject.setup(guid);
			this.addDataObject(fqClassName, guid, o);
			editorObject.syncWithDataObject();
			editorObject.init();
			return editorObject;
		} catch (InstantiationException e) {
			Core.getInstance().logMessage(ObjectDatabase.class.getName(),
					"Error instantiating " + fqClassName, e);
		} catch (IllegalAccessException e) {
			Core.getInstance().logMessage(ObjectDatabase.class.getName(),
					"Error accessing " + fqClassName, e);
		} catch (ClassNotFoundException e) {
			Core.getInstance().logMessage(ObjectDatabase.class.getName(),
					"Class " + fqClassName + " was not found.", e);
		}
		return null;
	}

	/*
	 * Generates a completely unique identifier for editor objects.
	 */
	private static String generateGUID(String className) {
		return className + ":" + System.nanoTime() + Math.random();
	}

	/**
	 * Exception the represents an error occurring when a data object of a given
	 * class has a name that already exists in that class's collection.
	 * 
	 * @deprecated
	 */
	public class NameAlreadyExistsException extends Exception {

		private static final long serialVersionUID = 2754018350024388898L;

		public NameAlreadyExistsException(String className, String existingName) {
			super("The name "
					+ existingName
					+ " already exists for the class "
					+ className);
		}

	}

}
